/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.coyote;

import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.PrivilegedGetTccl;
import org.apache.tomcat.util.security.PrivilegedSetTccl;

import java.security.AccessController;
import java.security.PrivilegedAction;

/**
 * Manages the state transitions for async requests.
 * <p>
 * <pre>
 * The internal states that are used are:
 * DISPATCHED       - Standard request. Not in Async mode.
 * STARTING         - ServletRequest.startAsync() has been called but the
 *                    request in which that call was made has not finished
 *                    processing.
 * STARTED          - ServletRequest.startAsync() has been called and the
 *                    request in which that call was made has finished
 *                    processing.
 * MUST_COMPLETE    - ServletRequest.startAsync() followed by complete() have
 *                    been called during a single Servlet.service() method. The
 *                    complete() will be processed as soon as the request
 *                    finishes.
 * COMPLETE_PENDING - ServletRequest.startAsync() has been called and before the
 *                    request in which that call was had finished processing,
 *                    complete() was called for a non-container thread. The
 *                    complete() will be processed as soon as the request
 *                    finishes. This is different to MUST_COMPLETE because of
 *                    differences required to avoid race conditions during error
 *                    handling.
 * COMPLETING       - The call to complete() was made once the request was in
 *                    the STARTED state. May or may not be triggered by a
 *                    container thread - depends if start(Runnable) was used.
 * TIMING_OUT       - The async request has timed out and is waiting for a call
 *                    to complete(). If that isn't made, the error state will
 *                    entered.
 * MUST_DISPATCH    - ServletRequest.startAsync() followed by dispatch() have
 *                    been called during a single Servlet.service() method. The
 *                    dispatch() will be processed as soon as the request
 *                    finishes.
 * DISPATCH_PENDING - ServletRequest.startAsync() has been called and before the
 *                    request in which that call was had finished processing,
 *                    dispatch() was called for a non-container thread. The
 *                    dispatch() will be processed as soon as the request
 *                    finishes. This is different to MUST_DISPATCH because of
 *                    differences required to avoid race conditions during error
 *                    handling.
 * DISPATCHING      - The dispatch is being processed.
 * ERROR            - Something went wrong.
 *
 * |----------------->------|
 * |                       \|/
 * |   |----------<-----E R R O R<-------------------------<-------------------------------|
 * |   |      complete() /|\/|\\ \--<---------------------------------<-----|              |
 * |   |                  |  |  \                                           |              |
 * |   |    |----->-------|  |   \----------->----------|                   |              |
 * |   |    |                |                          |dispatch()         |              |
 * |   |    |                |                         \|/                  ^              |
 * |   |    |                |          |--|timeout()   |                   |              |
 * |   |    |     post()     |          | \|/           |     post()        |              |
 * |   |    |    |---------- | -->DISPATCHED<---------- | --------------COMPLETING<-----|  |
 * |   |    |    |           |   /|\/|\ |               |                | /|\ /|\      |  |
 * |   |    |    |    |--->- | ---|  |  |startAsync()   |       timeout()|--|   |       |  |
 * |   |    ^    ^    |      |       |  |               |                       |       |  ^
 * |   |    |    |    |   |-- \ -----|  |   complete()  |                       |post() |  |
 * |   |    |    |    |   |    \        |     /-->----- | ---COMPLETE_PENDING->-|       |  |
 * |   |    |    |    |   |     \       |    /          |                               |  |
 * |   |    |    |    |   ^      \      |   /           |                               |  |
 * |  \|/   |    |    |   |       \    \|/ /   post()   |                               |  |
 * | MUST_COMPLETE-<- | - | --<----STARTING-->--------- | -------------|                ^  |
 * |         /|\      |   |  complete()  | \            |              |     complete() |  |
 * |          |       |   |              |  \           |    post()    |     /----------|  |
 * |          |       ^   |    dispatch()|   \          |    |-----|   |    /              |
 * |          |       |   |              |    \         |    |     |   |   /               |
 * |          |       |   |             \|/    \        |    |    \|/ \|/ /                |
 * |          |       |   |--<--MUST_DISPATCH-----<-----|    |--<--STARTED--->-------------|
 * |          |       | dispatched() /|\   |     \                / |
 * |          |       |               |    |      \              /  |
 * |          |       |               |    |       \            /   |
 * |          |       |               |    |post()  \           |   |
 * ^          ^       |               |    |       \|/          |   |
 * |          |       ^               |    |  DISPATCH_PENDING  |   |
 * |          |       |               |    |  |post()           |   |
 * |          |       |               |    |  |      |----------|   |
 * |          |       |               |    |  |      |  dispatch()  |
 * |          |       |               |    |  |      |              |
 * |          |       |post()         |    |  |      |     timeout()|
 * |          |       |dispatched()   |   \|/\|/    \|/             |
 * |          |       |---<---------- | ---DISPATCHING              |
 * |          |                       |     |    ^                  |
 * |          |                       |     |----|                  |
 * |          |                       |    timeout()                |
 * |          |                       |                             |
 * |          |                       |       dispatch()           \|/
 * |          |                       |-----------<-----------TIMING_OUT
 * |          |                                                 |   |
 * |          |-------<----------------------------------<------|   |
 * |                              complete()                        |
 * |                                                                |
 * |<--------<-------------------<-------------------------------<--|
 *                                 error()
 * </pre>
 */
public class AsyncStateMachine<S> {

	/**
	 * The string manager for this package.
	 */
	private static final StringManager sm =
			StringManager.getManager(Constants.Package);
	private volatile AsyncState state = AsyncState.DISPATCHED;
	// Need this to fire listener on complete
	private AsyncContextCallback asyncCtxt = null;
	private Processor<S> processor;
	public AsyncStateMachine(Processor<S> processor) {
		this.processor = processor;
	}

	public boolean isAsync() {
		return state.isAsync();
	}

	public boolean isAsyncDispatching() {
		return state.isDispatching();
	}

	public boolean isAsyncStarted() {
		return state.isStarted();
	}

	public boolean isAsyncTimingOut() {
		return state == AsyncState.TIMING_OUT;
	}

	public boolean isAsyncError() {
		return state == AsyncState.ERROR;
	}

	public boolean isCompleting() {
		return state.isCompleting();
	}

	public synchronized void asyncStart(AsyncContextCallback asyncCtxt) {
		if (state == AsyncState.DISPATCHED) {
			state = AsyncState.STARTING;
			this.asyncCtxt = asyncCtxt;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncStart()", state));
		}
	}

	/*
	 * Async has been processed. Whether or not to enter a long poll depends on
	 * current state. For example, as per SRV.2.3.3.3 can now process calls to
	 * complete() or dispatch().
	 */
	public synchronized SocketState asyncPostProcess() {
		if (state == AsyncState.COMPLETE_PENDING) {
			doComplete();
			return SocketState.ASYNC_END;
		} else if (state == AsyncState.DISPATCH_PENDING) {
			doDispatch();
			return SocketState.ASYNC_END;
		} else if (state == AsyncState.STARTING) {
			state = AsyncState.STARTED;
			return SocketState.LONG;
		} else if (state == AsyncState.MUST_COMPLETE) {
			asyncCtxt.fireOnComplete();
			state = AsyncState.DISPATCHED;
			return SocketState.ASYNC_END;
		} else if (state == AsyncState.COMPLETING) {
			asyncCtxt.fireOnComplete();
			state = AsyncState.DISPATCHED;
			return SocketState.ASYNC_END;
		} else if (state == AsyncState.MUST_DISPATCH) {
			state = AsyncState.DISPATCHING;
			return SocketState.ASYNC_END;
		} else if (state == AsyncState.DISPATCHING) {
			state = AsyncState.DISPATCHED;
			return SocketState.ASYNC_END;
		} else if (state == AsyncState.STARTED) {
			// This can occur if an async listener does a dispatch to an async
			// servlet during onTimeout
			return SocketState.LONG;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncPostProcess()", state));
		}
	}

	public synchronized boolean asyncComplete() {
		if (!ContainerThreadMarker.isContainerThread() && state == AsyncState.STARTING) {
			state = AsyncState.COMPLETE_PENDING;
			return false;
		} else {
			return doComplete();
		}
	}

	private synchronized boolean doComplete() {
		boolean doComplete = false;
		if (state == AsyncState.STARTING) {
			state = AsyncState.MUST_COMPLETE;
		} else if (state == AsyncState.STARTED || state == AsyncState.COMPLETE_PENDING) {
			state = AsyncState.COMPLETING;
			doComplete = true;
		} else if (state == AsyncState.TIMING_OUT ||
				state == AsyncState.ERROR) {
			state = AsyncState.MUST_COMPLETE;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncComplete()", state));

		}
		return doComplete;
	}

	public synchronized boolean asyncTimeout() {
		if (state == AsyncState.STARTED) {
			state = AsyncState.TIMING_OUT;
			return true;
		} else if (state == AsyncState.COMPLETING ||
				state == AsyncState.DISPATCHING ||
				state == AsyncState.DISPATCHED) {
			// NOOP - App called complete() or dispatch() between the the
			// timeout firing and execution reaching this point
			return false;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncTimeout()", state));
		}
	}

	public synchronized boolean asyncDispatch() {
		if (!ContainerThreadMarker.isContainerThread() && state == AsyncState.STARTING) {
			state = AsyncState.DISPATCH_PENDING;
			return false;
		} else {
			return doDispatch();
		}
	}

	private synchronized boolean doDispatch() {
		boolean doDispatch = false;
		if (state == AsyncState.STARTING ||
				state == AsyncState.TIMING_OUT ||
				state == AsyncState.ERROR) {
			// In these three cases processing is on a container thread so no
			// need to transfer processing to a new container thread
			state = AsyncState.MUST_DISPATCH;
		} else if (state == AsyncState.STARTED || state == AsyncState.DISPATCH_PENDING) {
			// A dispatch is always required.
			// If on a non-container thread, need to get back onto a container
			// thread to complete the processing.
			// If on a container thread the current request/response are not the
			// request/response associated with the AsyncContext so need a new
			// container thread to process the different request/response.
			state = AsyncState.DISPATCHING;
			doDispatch = true;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncDispatch()", state));
		}
		return doDispatch;
	}

	public synchronized void asyncDispatched() {
		if (state == AsyncState.DISPATCHING ||
				state == AsyncState.MUST_DISPATCH) {
			state = AsyncState.DISPATCHED;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncDispatched()", state));
		}
	}

	public synchronized boolean asyncError() {
		boolean doDispatch = false;
		if (state == AsyncState.STARTING ||
				state == AsyncState.STARTED ||
				state == AsyncState.DISPATCHED ||
				state == AsyncState.TIMING_OUT ||
				state == AsyncState.MUST_COMPLETE ||
				state == AsyncState.COMPLETING) {
			state = AsyncState.ERROR;
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncError()", state));
		}
		return doDispatch;
	}

	public synchronized void asyncRun(Runnable runnable) {
		if (state == AsyncState.STARTING || state == AsyncState.STARTED) {
			// Execute the runnable using a container thread from the
			// Connector's thread pool. Use a wrapper to prevent a memory leak
			ClassLoader oldCL;
			if (Constants.IS_SECURITY_ENABLED) {
				PrivilegedAction<ClassLoader> pa = new PrivilegedGetTccl();
				oldCL = AccessController.doPrivileged(pa);
			} else {
				oldCL = Thread.currentThread().getContextClassLoader();
			}
			try {
				if (Constants.IS_SECURITY_ENABLED) {
					PrivilegedAction<Void> pa = new PrivilegedSetTccl(
							this.getClass().getClassLoader());
					AccessController.doPrivileged(pa);
				} else {
					Thread.currentThread().setContextClassLoader(
							this.getClass().getClassLoader());
				}

				processor.getExecutor().execute(runnable);
			} finally {
				if (Constants.IS_SECURITY_ENABLED) {
					PrivilegedAction<Void> pa = new PrivilegedSetTccl(
							oldCL);
					AccessController.doPrivileged(pa);
				} else {
					Thread.currentThread().setContextClassLoader(oldCL);
				}
			}
		} else {
			throw new IllegalStateException(
					sm.getString("asyncStateMachine.invalidAsyncState",
							"asyncRun()", state));
		}

	}

	public synchronized void recycle() {
		// Ensure in case of error that any non-container threads that have been
		// paused are unpaused.
		notifyAll();
		asyncCtxt = null;
		state = AsyncState.DISPATCHED;
	}

	private static enum AsyncState {
		DISPATCHED(false, false, false, false),
		STARTING(true, true, false, false),
		STARTED(true, true, false, false),
		MUST_COMPLETE(true, true, true, false),
		COMPLETE_PENDING(true, true, false, false),
		COMPLETING(true, false, true, false),
		TIMING_OUT(true, true, false, false),
		MUST_DISPATCH(true, true, false, true),
		DISPATCH_PENDING(true, true, false, false),
		DISPATCHING(true, false, false, true),
		ERROR(true, true, false, false);

		private final boolean isAsync;
		private final boolean isStarted;
		private final boolean isCompleting;
		private final boolean isDispatching;

		private AsyncState(boolean isAsync, boolean isStarted, boolean isCompleting,
		                   boolean isDispatching) {
			this.isAsync = isAsync;
			this.isStarted = isStarted;
			this.isCompleting = isCompleting;
			this.isDispatching = isDispatching;
		}

		public boolean isAsync() {
			return isAsync;
		}

		public boolean isStarted() {
			return isStarted;
		}

		public boolean isDispatching() {
			return isDispatching;
		}

		public boolean isCompleting() {
			return isCompleting;
		}
	}
}
